package org.iamd.domain.meta;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.iamd.conf.ConfigurationManager;
import org.iamd.domain.business.ApplicationException;
import org.iamd.domain.business.BusinessController;
import org.iamd.domain.business.BusinessObject;
import org.iamd.domain.data.Data;
import org.iamd.domain.data.DataAccess;
import org.iamd.security.AccessContext;
import org.iamd.security.DefPermissions;
import org.iamd.security.ISecurityManager;
import org.iamd.security.SecurityException;
import org.iamd.security.SecurityServicesManager;
import org.iamd.util.CommonMsgKeys;
import org.iamd.util.Messenger;

/**
 * Сервиз за зареждане на метаданни, поддържане на номенклатурен кеш, презареждане на метаданни.
 * 
 * @author И. Атанасов
 */
public class MetadataService
		extends
		BusinessController<Data, DataAccess<Data, Object>, BusinessObject<Data, DataAccess<Data, Object>>> {

	/** Единствената инстанция - singleton pattern. */
	private static MetadataService instance;

	/**
	 * Връща единствената инстанция - singleton pattern.
	 * 
	 * @return инстанция на сервиза.
	 * 
	 * @throws ApplicationException
	 *             при неуспешно инстанциране
	 */
	public static MetadataService getInstance() throws ApplicationException {
		if (instance == null) {
			instance = new MetadataService();
		}
		return instance;
	}

	/** Мапинг между ключове на метаданни и техните инстанции (кеш). */
	private Map<INomTypes, Metadata> noms;

	/**
	 * Инициализация на кеша на метаданните.
	 * 
	 * @throws ApplicationException
	 *             при неуспешно инстанциране
	 */
	private MetadataService() throws ApplicationException {
		this.noms = new HashMap<INomTypes, Metadata>();
	}

	/** Изчиства номенклатурния кеш. */
	public void clearCache() {
		this.noms.clear();
	}

	/**
	 * Връща комплект именовани метаданни. Ако типа е <code>null</code>, методът връща
	 * <code>null</code> като резултат. Ако не е намерен резултат в кеша - прави се обръщение до
	 * съответното репозитори и метаданните се извличат от там. Ако текущия тип метаданни поддържа
	 * кеширане - той се добавя в кеша. Следва проверка от мениджъра на Сигурността, който филтрира
	 * елементите, до които текущия потребител няма достъп.
	 * 
	 * @param type
	 *            елемент от енумерацията на номенклатурите
	 * 
	 * @return зареденият комплект именовани метаданни. За да има резултат различен от
	 *         <code>null</code> е задължително всичките методи на входния параметър да са различни
	 *         от <code>null</code>
	 * 
	 * @throws ApplicationException
	 *             ако нещо се строши при кеширане
	 */
	@SuppressWarnings("unchecked")
	public Metadata getMetadata(INomTypes type) throws ApplicationException {
		Metadata result = null;
		if (isValidType(type)) {
			try {
				for (INomTypes nomType : this.noms.keySet()) {
					Metadata currentNom = this.noms.get(nomType);
					if (type == nomType) {
						result = currentNom;
						break;
					}
				}
				if (result == null) {
					result = findNomenclature(type);
					if (result != null && type.isCacheable()) {
						this.noms.put(type, result);
					}
				}
				if (result != null) {
					AMetadata<Data, DataAccess<Data, Object>> aMetadata = (AMetadata<Data, DataAccess<Data, Object>>) result;
					result = aMetadata.clone();
					ISecurityManager mng = SecurityServicesManager.getInstance().getService(
							ISecurityManager.class);
					List<Selectable> children = result.getChildren();
					List<Selectable> filteredChildren = filterChildren(children, mng);
					children.clear();
					children.addAll(filteredChildren);
				}
			} catch (SecurityException e) {
				throw new ApplicationException(CommonMsgKeys.M0005_LOAD_METADATA_FAILED,
						new Object[] { type.getMetadataName() }, e);
			}
		}
		return result;
	}

	/**
	 * По системно име на елемент от номенклатура и вид на номенклатурата връща конкретния елемент.
	 * Ако подадения елемент е <code>null</code>, или това важи за неговия номенклатурен тип, или
	 * системното му име - методът връща <code>null</code>.
	 * 
	 * @param nomValue
	 *            елемент от номенклатура
	 * 
	 * @return зареден елемент или <code>null</code> ако не е намерен такъв
	 * 
	 * @throws ApplicationException
	 *             при неуспешно зареждане на номенклатура, в която ще се проверява
	 */
	public Selectable getMetadataSubtree(INomValues nomValue) throws ApplicationException {
		if (nomValue != null) {
			INomTypes nomType = nomValue.getNomType();
			String sysName = nomValue.getSystemName();
			return (isValidType(nomType) && sysName != null) ? recursiveSearchForParent(
					getMetadata(nomType).getChildren(), sysName, null) : null;
		}
		return null;
	}

	/**
	 * Опреснява кеша за дадена номенклатура. Ако елемента не е валиден (
	 * {@link MetadataService#isValidType(INomTypes)}), или не поддържа кеширане - не прави нищо.
	 * 
	 * @param type
	 *            елемент от енумерацията на номенклатурите
	 * 
	 * @throws ApplicationException
	 *             при неуспешно опресняване
	 */
	public void refreshNomenclature(INomTypes type) throws ApplicationException {
		if (isValidType(type) && type.isCacheable()) {
			String base = type.getMetadataName();
			Object[] objParam = new Object[] { base };
			for (INomTypes nomType : this.noms.keySet()) {
				Metadata currentNom = this.noms.get(nomType);
				if (base.equalsIgnoreCase(currentNom.getMetaname())) {
					Messenger.logInfo(CommonMsgKeys.M0013_REFRESH_METADATA, objParam);
					try {
						this.noms.put(nomType, findNomenclature(type));
						Messenger.logInfo(CommonMsgKeys.M0014_REFRESH_METADATA_COMPLETED, objParam);
					} catch (ApplicationException e) {
						throw new ApplicationException(CommonMsgKeys.M0015_REFRESH_METADATA_FAILED,
								objParam, e);
					}
					break;
				}
			}
		}
	}

	/**
	 * Връща винаги <code>null</code>.
	 * 
	 * @see org.iamd.domain.business.BusinessWorker#getDAOClass()
	 */
	@Override
	protected Class<DataAccess<Data, Object>> getDAOClass() {
		return null;
	}

	/**
	 * Орязва от пълното дърво с номенклатурни елементи тези, които не са позволени за текущия
	 * потребител.
	 * 
	 * @param children
	 *            списък от номенклатурни елементи
	 * @param mng
	 *            инстанция на мениджъра на сигурността
	 * 
	 * @return филтрираните деца като дърво
	 */
	@SuppressWarnings("unchecked")
	private List<Selectable> filterChildren(List<? extends Selectable> children,
			ISecurityManager mng) {
		List<Selectable> result = new ArrayList<Selectable>();
		for (Selectable child : children) {
			try {
				mng.checkPermission(child, DefPermissions.READ);
				List<Selectable> nextChildren = (List<Selectable>) child.getChildren();
				List<Selectable> filteredChildren = filterChildren(nextChildren, mng);
				nextChildren.clear();
				nextChildren.addAll(filteredChildren);
				result.add(child);
			} catch (SecurityException e) {
				// do nothing, it is not permitted
			}
		}
		return result;
	}

	/**
	 * По подадено име извлича номенклатурата от съответното репозитори, след това я опакова в
	 * зависимост дали е линейна или йерархична, и я връща като резултат.
	 * 
	 * @param nomType
	 *            елемент от енумерацията на номенклатурите
	 * 
	 * @return заредената номенклатура. Ако {@link INomTypes#getMetadataInstance()} е
	 *         <code>null</code> - връща <code>null</code>
	 * 
	 * @throws ApplicationException
	 *             ако нещо се строши при кеширане
	 */
	@SuppressWarnings("unchecked")
	private Metadata findNomenclature(INomTypes nomType) throws ApplicationException {
		AccessContext.get().pushUniqueId(ConfigurationManager.getInstance().getAdminID());
		try {
			Metadata nom = nomType.getMetadataInstance();
			return nom != null ? (Metadata) ((AMetadata<Data, DataAccess<Data, Object>>) nom)
					.load() : null;
		} catch (ApplicationException e) {
			throw new ApplicationException(CommonMsgKeys.M0005_LOAD_METADATA_FAILED,
					new Object[] { nomType.getMetadataName() }, e);
		} finally {
			AccessContext.get().popUniqueId();
		}
	}

	/**
	 * @param nomType
	 *            номенклатурен тип
	 * 
	 * @return <code>true</code> ако типа е различен от <code>null</code>, както и
	 *         {@link INomTypes#getMetadataName()} не връща <code>null</code>
	 */
	private boolean isValidType(INomTypes nomType) {
		return nomType != null && nomType.getMetadataName() != null;
	}

	/**
	 * Рекурсивно обхожда номенклатурата, търсейки подадения като параметър системно име на елемент.
	 * 
	 * @param children
	 *            децата на текущото ниво
	 * @param sysName
	 *            системно име на елемента
	 * @param foundElem
	 *            намерен елемент
	 * 
	 * @return намерения елемент
	 */
	@SuppressWarnings("all")
	private Selectable recursiveSearchForParent(List<? extends Selectable> children,
			String sysName, Selectable foundElem) {
		if (foundElem == null) {
			for (Selectable une : children) {
				if (une.getSystemName().equals(sysName)) {
					foundElem = une;
					break;
				}
				foundElem = recursiveSearchForParent(une.getChildren(), sysName, foundElem);
			}
		}
		return foundElem;
	}

}